跳到主要内容

Protobuf 协议

proto3 官方文档介绍(谷歌的文档写的就是棒

Protobuf 是什么?

Protocol Buffers 是一种与语言、平台无关,可扩展的序列化结构化数据的方法,常用于通信协议,数据存储等等。相较于 JSON、XML,它更小、更快、更简单。

Protocol Buffers 文件以 .proto 结尾

定义一个类型

创建一个 Search.proto

syntax = "proto3";

message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}

第一行标识版本号,如果不写则默认使用 proto2

可以看到这些字段分配了一个唯一的数字编号,这些标识号是用来在消息的二进制格式中识别各个字段的,这个数字的顺序不能随便改变,每个消息内唯一即可,不同的消息定义可以拥有相同的标识号。

注意:[1,15] 之内的标识号在编码的时候会占用一个字节。[16,2047] 之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15] 之内的标识号。切记:要为将来有可能添加的、频繁出现的字段预留一些标识号。

可以在单个 .proto 里面定义多个结构,以及它内部可以使用 ///* ... */ 注释

/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */

message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3; // Number of results to return per page.
}

message SearchResponse {
...
}

默认值

  • 字符串类型默认为空字符串
  • 字节类型默认为空字节
  • 布尔类型默认 false
  • 数值类型默认为 0 值
  • enums 类型默认为第一个定义的枚举值,必须是 0

reserved 关键字

可以使用 reserved 关键字指定保留字段名和标签。

当一些删掉的字段,但是还有可能有人使用时可以加上 reserved 关键字,这样编译器就不会报错,例如下面指定了哪些字段已经被删除了,但是还能使用

message Foo {
reserved 2, 15, 9 to 11; // 可以指定编号
reserved "foo", "bar"; // 也可以指定字段名称
}

不过注意,不能在同一个保留语句中混合字段名和字段编号。

proto3 不支持 proto2 中的 required 和 optional 关键字

repeated 关键字

如果一个字段被重复,该字段可以被重复任意次数(包括零次)。重复值的顺序将保存在协议缓冲区中。可以将重复字段看作动态调整大小的数组。

就是定义一个数组

message Book {
repeated string names = 0;
}

导入定义(import)

可以使用 import 语句导入使用其它描述文件中声明的类型:

import "others.proto";

默认情况,只能使用直接导入的 .proto 文件内的定义。但是有时候需要移动 .proto 文件到其它位置,为了避免更新所有相关文件,可以在 原位置 放置一个模型 .proto 文件,使用 public 关键字,转发所有对新文件内容的引用,例如:

// old.proto

// 客户端导入的原来的proto文件
import public "new.proto";
import "other.proto";
// client.proto

import "old.proto";
// 这里可以使用 old.proto 和 new.proto 文件中的定义,但是不能使用 other.proto 文件中的定义。

protocol 编译器会在编译命令中 -I / --proto_path 参数指定的目录中查找导入的文件,如果没有指定该参数,默认在当前目录中查找。

定义服务(Service)

如果想要将消息类型用在 RPC(远程方法调用)系统中,可以在 .proto 文件中定义一个 RPC 服务接口,protocol 编译器会根据所选择的不同语言生成服务接口代码。

例如,想要定义一个 RPC 服务并具有一个方法,该方法接收 SearchRequest 并返回一个 SearchResponse,此时可以在 .proto 文件中进行如下定义:

service SearchService {
rpc Search (SearchRequest) returns (SearchResponse) {}
}

生成的接口代码作为客户端与服务端的约定,服务端必须实现定义的所有接口方法,客户端直接调用同名方法向服务端发起请求。

在 Go 中使用

如果要序列化一个复杂的对象的时候,可以使用以下几种方案

  • 使用 Golang 提供的 gobs 包,但是它没办法跨语言
  • 自己创建一个字符编码,但是只适合少量的数据
  • 序列化成 XML

当然以上都有缺点,否则怎么会有 Protobuf 呢?官方的 示例代码

安装 protoc

安装编译器,参考 官方文档

编译安装:

# 这里可以只下载对应的编译器的
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.19.1/protoc-3.19.1-linux-x86_64.zip
unzip protoc-3.19.1-linux-x86_64.zip -d protobuf-3.19.1/

# 添加这个执行文件
vim ~/.bashrc
export PROTOBUF=/home/alsritter/tool/go/protobuf-3.19.1/
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin:$PROTOBUF/bin

# 更新
source ~/.bashrc
# 检查是否安装完成
protoc --version

安装 Golang 的编译插件

go get -u github.com/golang/protobuf/protoc-gen-go

将 Protoc Plugin 的可执行文件从 $GOPATH 中移动到 $GOBIN

mv $GOPATH/bin/protoc-gen-go $GOROOT/bin/

创建一个 Proto 结构

创建一个 addressbook.proto 文件

syntax = "proto3";
package stgrpc; // 自定义一个包名称

import "google/protobuf/timestamp.proto";

这个包名称有助于避免不同项目之间的冲突,在 proto 文件中使用 package 关键字声明包名,默认转换成 go 中的包名与此一致,如果需要指定不一样的包名,可以使用 go_package 选项:

option go_package = "./;hello"; // 前面是导出路径, ; 后面是包名

这个 go_package 选项定义了该文件生成的所有文件生成的路径,最后一个路径是 Go 的包名

下面开始定义 message

syntax = "proto3";
package stgrpc; // 自定义一个包名称
option go_package = "./;protobuf"; // 生成到哪个路径里面去(这里是生成到当前目录,但是使用 protobuf 包)

import "google/protobuf/timestamp.proto";


message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;

enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}

message PhoneNumber {
string number = 1;
PhoneType type = 2;
}

repeated PhoneNumber phones = 4;

google.protobuf.Timestamp last_updated = 5;
}

// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}

protoc 编译

执行 protoc --help(查看帮助文档),抽出几个常用的命令进行讲解

1、--go_out:设置 Go 代码输出的目录 2、--proto_path=PATH:指定 import 搜索的目录,可指定多个,如果不指定则默认当前工作目录

: 冒号充当分隔符的作用,后跟所需要的参数集。

protoc --go_out=. *.proto

如下,它生成到指定的 go_package 路径下面去了,这个生成的文件就不去看了

现在就可以直接使用这个结构了,如下创建

import (
"fmt"
"stgrpc/protobuf"
)

func main() {
p := protobuf.Person{
Id: 1234,
Name: "John Doe",
Email: "jdoe@example.com",
Phones: []*protobuf.Person_PhoneNumber{
{Number: "555-4321", Type: protobuf.Person_HOME},
},
}

fmt.Printf("%v \n", p)
}

输出:

{{{} [] [] <nil>} 0 [] John Doe 1234 jdoe@example.com [number:"555-4321"  type:HOME] <nil>} 

写消息 Serialize

使用协议缓冲的全部目的就是序列化数据,这样就可以在其他地方解析数据。

在 Go 中,可以使用 proto 库的 Marshal 函数序列化协议缓冲区数据。

// serialize

package main

import (
"io/ioutil"
"log"
"stgrpc/protobuf"

"google.golang.org/protobuf/proto"
)


func main() {
book := &protobuf.AddressBook{}
// 给这个 book 添加几个 People
book.People = append(book.People,
&protobuf.Person{
Id: 1234,
Name: "John Doe",
Email: "jdoe@example.com",
Phones: []*protobuf.Person_PhoneNumber{
{Number: "555-4321", Type: protobuf.Person_HOME},
},
},
&protobuf.Person{
Id: 5678,
Name: "Alsritter",
Email: "als@example.com",
Phones: []*protobuf.Person_PhoneNumber{
{Number: "777-4321", Type: protobuf.Person_WORK},
},
})

// Write the new address book back to disk.
out, _ := proto.Marshal(book)
// This test.txt is a binary file.
if err := ioutil.WriteFile("test.txt", out, 0644); err != nil {
log.Fatalln("Failed to write address book:", err)
}
}

读数据 Deserialize

反序列化也很简单

package main

import (
"fmt"
"io/ioutil"
"log"
"stgrpc/protobuf"

"google.golang.org/protobuf/proto"
)

func main() {
// Read the existing address book.
in, err := ioutil.ReadFile("test.txt")
if err != nil {
log.Fatalln("Error reading file:", err)
}
book := &protobuf.AddressBook{}
if err := proto.Unmarshal(in, book); err != nil {
log.Fatalln("Failed to parse address book:", err)
}

fmt.Printf("book: %v \n", book)
}

输出:

book: people:{name:"John Doe"  id:1234  email:"jdoe@example.com"  phones:{number:"555-4321"  type:HOME}}  people:{name:"Alsritter"  id:5678  email:"als@example.com"  phones:{number:"777-4321"  type:WORK}}

安装插件

go get -u github.com/golang/protobuf/protoc-gen-go
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger

GUI 客户端

BloomRPC